#!/usr/bin/python2.6
"""SVNPro - A fast GUI for SVN repository management

Currently supports:
* Adding files to the repository
* Deleting of files from the repository
* Committing changes to the server
* Updating to the latest revision
* Checking out a working copy from a server
* Modifiying a file's properties
* Viewing in flat mode or tree mode
* Editing the configuration
* Editing a file in an external editor

Requirements:
Python/2.6
PyGTK/2.16
"""

import pygtk
pygtk.require("2.0")
import ConfigParser, gtk, mimetypes, os, subprocess, sys, tempfile, threading, time, traceback
basedir = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0])))
getxmlpath = lambda name: os.path.join(basedir, name)
CONFIGFILE = getxmlpath("config.conf")
class app:
	def _guess_type(self, fname):
		type, enc = mimetypes.guess_type(fname)
		if type is None:
			p = subprocess.Popen(["svn", "pg", "svn:mime-type", fname], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
			self._write("Execute: Get property\n")
			self._write("svn pg \"svn:mime-type\" \"%s\"\n" % fname)
			stdout, stderr = p.communicate()
			self._write(stdout.rstrip("\r\n") + "\n")
			self._write("Done\n\n")
			if stdout.strip():
				type = stdout.strip()
			else:
				type = "Unknown"
		if enc is not None:
			type += " (%s)" % enc
		return type
	def _open(self):
		if not os.path.exists(os.path.join(self.wc, ".svn")):
			self._error("Directory is not a valid working copy", "%s does not exist" % os.path.join(self.wc, ".svn"))
		else:
			os.chdir(self.wc)
			self._stat()
	def _error(self, msga, msgb):
		dialog, labela, labelb = self._build(getxmlpath("error.xml"), "dialog1", "label1", "label3")
		labela.set_text(msga)
		labelb.set_text(msgb)
		dialog.show_all()
		dialog.run()
		dialog.destroy()
	def _info(self, msg):
		dialog, label = self._build(getxmlpath("info.xml"), "dialog1", "label1")
		label.set_text(msg)
		dialog.show_all()
		dialog.run()
		dialog.destroy()
	def _build(self, file, *widgets):
		builder = gtk.Builder()
		builder.add_from_file(file)
		builder.connect_signals(self, None)
		self.objects.append(builder.get_objects())
		return tuple([builder.get_object(widget) for widget in widgets])
	def _write(self, s):
		self.output.insert(self.output.get_end_iter(), s)
	def _stat(self):
		self.treestore.clear()
		self.paths = {}
		self.ipaths = {}
		if self.flat:
			for root, dirs, files in os.walk(self.wc):
				try:
					dirs.remove(".svn")
				except ValueError:
					pass
				dirs.sort()
				for dir in dirs:
					iter = self.treestore.append([dir, "", os.path.relpath(os.path.join(root, dir)), "Directory"])
					self.paths[os.path.relpath(os.path.join(root, dir))] = iter
					self.ipaths[self.treestore.get_path(iter)] = os.path.relpath(os.path.join(root, dir))
				files.sort()
				for file in files:
					typestr = self._guess_type(os.path.relpath(os.path.join(root, file)))
					iter = self.treestore.append([file, "", os.path.relpath(os.path.join(root, file)), typestr])
					self.paths[os.path.relpath(os.path.join(root, file))] = iter
					self.ipaths[self.treestore.get_path(iter)] = os.path.relpath(os.path.join(root, file))
		else:
			iters = {self.wc : None}
			for root, dirs, files in os.walk(self.wc):
				try:
					dirs.remove(".svn")
				except ValueError:
					pass
				dirs.sort()
				for dir in dirs:
					iter = self.treestore.append(iters[root], [dir, "", os.path.relpath(os.path.join(root, dir)), "Directory"])
					iters[os.path.join(root, dir)] = iter
					self.paths[os.path.relpath(os.path.join(root, dir))] = iter
					self.ipaths[self.treestore.get_path(iter)] = os.path.relpath(os.path.join(root, dir))
				files.sort()
				for file in files:
					typestr = self._guess_type(os.path.relpath(os.path.join(root, file)))
					iter = self.treestore.append(iters[root], [file, "", os.path.relpath(os.path.join(root, file)), typestr])
					self.paths[os.path.relpath(os.path.join(root, file))] = iter
					self.ipaths[self.treestore.get_path(iter)] = os.path.relpath(os.path.join(root, file))
		p = subprocess.Popen(["svn", "st"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
		self._write("Execute: Status\n")
		self._write("svn st\n")
		stdout, stderr = p.communicate()
		self._write(stdout.rstrip("\r\n") + "\n")
		for line in stdout.splitlines():
			if line:
				flags = line[:6]
				name = line[7:]
				iter = self.paths[name]
				self.treestore.set_value(iter, 1, "")
				addflag = lambda flag: self.treestore.set_value(iter, 1, self.treestore.get_value(iter, 1) + flag + "; ")
				if flags[0] == "A":
					addflag("Added")
				elif flags[0] == "C":
					addflag("Conflicted")
				elif flags[0] == "D":
					addflag("Deleted")
				elif flags[0] == "I":
					addflag("Ignored")
				elif flags[0] == "M":
					addflag("Modified")
				elif flags[0] == "R":
					addflag("Replaced")
				elif flags[0] == "?":
					addflag("Not under version control")
				elif flags[0] == "!":
					addflag("Missing/incomplete")
				elif flags[0] == "~":
					addflag("Obstructed")
				if flags[1] == "C":
					addflag("Properties conflicted")
				elif flags[1] == "M":
					addflag("Properties modified")
				if flags[2] == "L":
					addflag("Locked")
				if flags[3] == "+":
					addflag("History scheduled for commit")
				if flags[4] == "S":
					addflag("Switched")
				if flags[5] == "K":
					addflag("Lock token present")
		self._write("Done\n\n")
	def __init__(self):
		pass
	def main(self):
		self.config = ConfigParser.ConfigParser()
		self.config.read(CONFIGFILE)
		self.objects = []
		self.flat = False
		window, self.treestore, self.treeview, self.output =\
			self._build(getxmlpath("main.xml"), "window1", "treestore1", "treeview1", "textbuffer1")
		window.maximize()
		window.show_all()
		self.wc = None
		if self.config.has_option("misc", "current-wc"):
			self.wc = self.config.get("misc", "current-wc")
			self._open()
		gtk.main()
	def quit(self, *gtkargs):
		if self.wc is not None:
			self.config.set("misc", "current-wc", self.wc)
		f = open(CONFIGFILE, "w")
		self.config.write(f)
		f.close()
		gtk.main_quit()
	def open(self, *gtkargs):
		filechooser, = self._build(getxmlpath("open_wc.xml"), "filechooserdialog1")
		filechooser.show_all()
		resp = filechooser.run()
		if resp == 1:
			self.wc = filechooser.get_filename()
			filechooser.destroy()
			self._open()
		else:
			filechooser.destroy()
	def add(self, *gtkargs):
		row, col = self.treeview.get_cursor()
		fname = self.ipaths[row]
		p = subprocess.Popen(["svn", "add", fname], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
		self._write("Execute: Add\n")
		self._write("svn add \"%s\"\n" % fname)
		stdout, stderr = p.communicate()
		self._write(stdout.rstrip("\r\n") + "\n")
		self._write("Done\n\n")
		self._stat()
	def delete(self, *gtkargs):
		row, col = self.treeview.get_cursor()
		fname = self.ipaths[row]
		p = subprocess.Popen(["svn", "del", fname, "--force"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
		self._write("Execute: Delete\n")
		self._write("svn del \"%s\" --force\n" % fname)
		stdout, stderr = p.communicate()
		self._write(stdout.rstrip("\r\n") + "\n")
		self._write("Done\n\n")
		self._stat()
	def commit(self, *gtkargs):
		dialog, buffer = self._build(getxmlpath("logmsg.xml"), "dialog1", "textbuffer1")
		dialog.show_all()
		resp = dialog.run()
		if resp == 0:
			fp, fn = tempfile.mkstemp()
			f = open(fn, "w")
			f.write(buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter()))
			f.close()
			dialog.destroy()
			p = subprocess.Popen(["svn", "ci", "-F", fn], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
			self._write("Execute: Commit\n")
			self._write("svn ci -F \"%s\"\n" % fn)
			stdout, stderr = p.communicate()
			self._write(stdout.rstrip("\r\n") + "\n")
			os.remove(fn)
			self._write("Done\n\n")
			self._info(stdout)
			self._stat()
		else:
			dialog.destroy()
	def update(self, *gtkargs):
		p = subprocess.Popen(["svn", "up"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
		self._write("Execute: Update\n")
		self._write("svn up\n")
		stdout, stderr = p.communicate()
		self._write(stdout.rstrip("\r\n") + "\n")
		self._write("Done\n\n")
		self._info(stdout)
		self._stat()
	def refresh(self, *gtkargs):
		self._stat()
	def edit(self, *gtkargs):
		row, col = self.treeview.get_cursor()
		fname = self.ipaths[row]
		cmd = [self.config.get("misc", "editor"), fname]
		p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
		sub_thread = threading.Thread(None, p.communicate, "SVNPro:Edit")
		sub_thread.start()
	def toggle_flat(self, *gtkargs):
		self.flat = not self.flat
		if self.flat:
			self.treestore = gtk.ListStore(str, str, str, str)
		else:
			self.treestore = gtk.TreeStore(str, str, str, str)
		self.treeview.set_model(self.treestore)
		self._stat()
	def new_property(self, *gtkargs):
		dialog, entryn, bufferv = self._build(getxmlpath("newprop.xml"), "dialog1", "entry1", "textbuffer1")
		dialog.show_all()
		resp = dialog.run()
		if resp == 1:
			name = entryn.get_text()
			value = bufferv.get_text(bufferv.get_start_iter(), bufferv.get_end_iter())
			p = subprocess.Popen(["svn", "ps", name, value, self.propfname], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
			self._write("Execute: Set property\n")
			self._write("svn ps \"%s\" \"%s\" \"%s\"\n" % (name, value, self.propfname))
			stdout, stderr = p.communicate()
			self._write(stdout.rstrip("\r\n") + "\n")
			self._write("Done\n\n")
			self._update_props()
		dialog.destroy()
	def edit_property(self, *gtkargs):
		row, col = self.propview.get_cursor()
		iter = self.propstore.get_iter(row)
		name = self.propstore.get_value(iter, 0)
		value = self.propstore.get_value(iter, 1)
		dialog, buffer = self._build(getxmlpath("editprop.xml"), "dialog1", "textbuffer1")
		buffer.set_text(value)
		dialog.show_all()
		resp = dialog.run()
		if resp == 1:
			value = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter())
			p = subprocess.Popen(["svn", "ps", name, value, self.propfname], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
			self._write("Execute: Set property\n")
			self._write("svn ps \"%s\" \"%s\" \"%s\"\n" % (name, value, self.propfname))
			stdout, stderr = p.communicate()
			self._write(stdout.rstrip("\r\n") + "\n")
			self._write("Done\n\n")
			self._update_props()
		dialog.destroy()
	def delete_property(self, *gtkargs):
		row, col = self.propview.get_cursor()
		iter = self.propstore.get_iter(row)
		name = self.propstore.get_value(iter, 0)
		p = subprocess.Popen(["svn", "pd", name, self.propfname], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
		self._write("Execute: Delete property\n")
		self._write("svn pd \"%s\" \"%s\"\n" % (name, self.propfname))
		stdout, stderr = p.communicate()
		self._write(stdout.rstrip("\r\n") + "\n")
		self._write("Done\n\n")
		self._update_props()
	def _update_props(self):
		self.propstore.clear()
		p = subprocess.Popen(["svn", "pl", self.propfname], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
		self._write("Execute: List properties\n")
		self._write("svn pl \"%s\"\n" % self.propfname)
		stdout, stderr = p.communicate()
		self._write(stdout.rstrip("\r\n") + "\n")
		self._write("Done\n\n")
		names = []
		for line in stdout.splitlines():
			if line.startswith("  "):
				name = line[2:]
				names.append(name)
		for name in names:
			p = subprocess.Popen(["svn", "pg", name, self.propfname], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
			self._write("Execute: Get property\n")
			self._write("svn pg \"%s\" \"%s\"\n" % (name, self.propfname))
			stdout, stderr = p.communicate()
			self._write(stdout.rstrip("\r\n") + "\n")
			self._write("Done\n\n")
			value = stdout.strip()
			iter = self.propstore.append([name, value])
	def properties(self, *gtkargs):
		row, col = self.treeview.get_cursor()
		if row is None:
			return
		self.propfname = self.ipaths[row]
		dialog, self.propview, self.propstore = self._build(getxmlpath("props.xml"), "dialog1", "treeview1", "liststore1")
		self._update_props()
		dialog.show_all()
		resp = dialog.run()
		dialog.destroy()
		self._stat()
	def checkout(self, *gtkargs):
		dialog, entry1, entry2, entry3 = self._build(getxmlpath("coparams.xml"), "dialog1", "entry1", "entry2", "entry3")
		dialog.show_all()
		resp = dialog.run()
		if resp != 1:
			dialog.destroy()
			return
		url = entry1.get_text()
		un = entry2.get_text()
		pw = entry3.get_text()
		dialog.destroy()
		fc, = self._build(getxmlpath("cofname.xml"), "filechooserdialog1")
		fc.show_all()
		resp = fc.run()
		if resp != 1:
			fc.destroy()
			return
		fname = fc.get_filename()
		fc.destroy()
		p = subprocess.Popen(["svn", "co", url, fname, "--username", un, "--password", pw],\
			stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
		self._write("Execute: Checkout\n")
		self._write("svn co \"%s\" \"%s\" --username \"%s\" --password \"*****\"\n" % (url, fname, un))
		stdout, stderr = p.communicate()
		self._write(stdout.rstrip("\r\n") + "\n")
		self._write("Done\n\n")
		self._info(stdout)
		self.wc = fname
		self._open()
	def prefs(self, *gtkargs):
		dialog, entry1 = self._build(getxmlpath("prefs.xml"), "dialog1", "entry1")
		entry1.set_text(self.config.get("misc", "editor"))
		dialog.show_all()
		resp = dialog.run()
		if resp != 1:
			dialog.destroy()
			return
		self.config.set("misc", "editor", entry1.get_text())
		dialog.destroy()
def main():
	a = app()
	def updater():
		while True:
			time.sleep(600)
			a.update()
	main_thread = threading.Thread(None, a.main, "SVNPro:Main")
	#updater_thread = threading.Thread(None, updater, "SVNPro:Updater")
	#updater_thread.setDaemon(True)
	main_thread.start()
	#updater_thread.start()
if __name__ == "__main__":
	main()
